package bg.btanev.eightyfour.server;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 *
 * <p>
 * A simple <a href="http://en.wikipedia.org/wiki/Decorator_pattern">decorator</a> over the GWT's
 * {@link RemoteServiceServlet}. It parses the request URL and tries to extract Spring bean name from it.
 * </p>
 *
 * <p>
 * <em>idea from the <a href="http://code.google.com/p/spring4gwt/">spring4gwt</a> project</em>
 * </p>
 *
 * @author B.Tanev
 *
 */
public class SpringGWTServlet extends RemoteServiceServlet {
	/**
	 *
	 */
	private static final long serialVersionUID = -7028920809362756743L;

	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(SpringGWTServlet.class);

	@Override
	public void init() {
		logger.info("init() - Spring-GWT integration servlet initialized."); //$NON-NLS-1$
	}

	/**
	 * <p>
	 * Tries to initialize a Spring bean that represents a service to process the invocation.
	 * </p>
	 *
	 * <p>
	 * <strong>Original JavaDoc:</strong><br />
	 * {@inheritDoc}
	 * </p>
	 */
	@Override
	public String processCall(String payload) throws SerializationException {
		final Object handler = getSpringBean(getThreadLocalRequest());
		if (handler == null) {
			logger.warn("processCall(String) - Spring bean not found. Call processing delegated to super class."); //$NON-NLS-1$
			return super.processCall(payload);
		}

		try {
			RPCRequest rpcRequest = RPC.decodeRequest(payload, handler.getClass(), this);
			onAfterRequestDeserialized(rpcRequest);

			logger
					.info(
							"processCall(String) - Invoking: {}.{};", handler.getClass().getName(), rpcRequest.getMethod().getName()); //$NON-NLS-1$

			return RPC.invokeAndEncodeResponse(handler, rpcRequest.getMethod(), rpcRequest.getParameters(), rpcRequest
					.getSerializationPolicy());
		} catch (IncompatibleRemoteServiceException ex) {
			log("An IncompatibleRemoteServiceException was thrown while processing this call.", ex); //$NON-NLS-1$
			return RPC.encodeResponseForFailure(null, ex);
		}
	}

	/**
	 * Determine Spring bean to handle request based on request URL, e.g. a request ending in /myService will be handled
	 * by bean with name "myService".
	 *
	 * @param request
	 * @return handler bean
	 */
	private Object getSpringBean(final HttpServletRequest request) {
		assert (request != null);

		Object bean = null;
		String service = getServiceBeanName(request.getRequestURI());
		if (service != null) {
			bean = getBean(service);
			if (!(bean instanceof RemoteService)) {
				throw new IllegalArgumentException(
						"Spring bean is not a GWT RemoteService: " + service + " (" + bean + ")"); //$NON-NLS-1$
			}

			logger.info("getSpringBean(HttpServletRequest) - Bean for service '{}' is '{}'.", service, bean); //$NON-NLS-1$
		}

		return bean;
	}

	/**
	 * Parse the service name from the request URL.
	 *
	 * @param request
	 * @return bean name
	 */
	private String getServiceBeanName(final String requestURL) {
		assert (requestURL != null && requestURL.length() > 0);
		String service = null;

		logger
				.info("getServiceBeanName(HttpServletRequest) - Extractiong spring service bean name from url: " + requestURL); //$NON-NLS-1$

		int slashPos = requestURL.lastIndexOf("/");
		if ((slashPos != -1) && (slashPos != (requestURL.length() - 1))) {
			service = requestURL.substring(slashPos + 1);
		}

		logger.info("getServiceBeanName(HttpServletRequest) - extracted service bean name: " + service); //$NON-NLS-1$
		return service;
	}

	/**
	 * Look up a spring bean with the specified name in the current web application context.
	 *
	 * @param beanName
	 *            bean name
	 * @return the bean
	 */
	private Object getBean(final String beanName) {
		assert (beanName != null && beanName.length() > 0);

		WebApplicationContext applicationContext = WebApplicationContextUtils
				.getWebApplicationContext(getServletContext());

		if (applicationContext == null) {
			throw new IllegalStateException("No Spring web application context found"); //$NON-NLS-1$
		}
		if (!applicationContext.containsBean(beanName)) {
			throw new IllegalArgumentException("Spring bean not found: " + beanName); //$NON-NLS-1$
		}

		Object bean = applicationContext.getBean(beanName);
		assert (bean != null);
		logger.info("getBean(String) - Bean '{}' retrieved from Spring container.", bean); //$NON-NLS-1$

		return bean;
	}
}
